# 网络服务 http server
30 秒速记
http.createServer只干两件事:管连接、把报文解析成req/res;路由、body解析、校验都是你自己的事(或者交给框架)- 超时分几层:
headersTimeout(收请求头,默认60s)、requestTimeout(收完整请求,v18起默认300s)、keepAliveTimeout(空闲长连接,默认5s)、server.timeout(socket不活动,v13起默认0不限) - 要有明确策略的事件:
error(监听失败)、clientError(报文坏了)、upgrade(WebSocket)、connect(代理隧道) - 多进程监听同一端口靠
cluster或平台负载均衡,进程之间内存不共享,会话和缓存要放Redis这类外部存储
Node 的 http.Server 是很薄的一层:接连接、解析报文、把 req 和 res 交给你,剩下的路由、解析 body、超时、优雅关闭都得自己管。 Express、Koa 本质上就是在 request 回调里帮你把这些补上。面试里常被追问的是超时,它不是一个值,而是分阶段的好几个:收请求头、收完整请求、长连接空闲、socket 不活动,各防一种慢攻击或泄漏。线上还要留意 keepAliveTimeout 要比前面负载均衡的空闲超时长,不然会偶发 502。
理解「网络服务 http server」时,要先明确输入、运行阶段和可观察输出。这里的交互实验会分别执行正常路径与失败路径,并把 Node.js 的关键状态变化和同屏代码对应起来;不能只记命令名称。
工程中还要验证退出码、资源清理和异常输入。属于 network 场景的配置应保持最小可复现,并在开发环境与 CI 中使用相同命令复跑,避免把平台差异或隐式默认值带到线上。
一个能上线的最小服务,比 res.end('ok') 多不了几行,但这几行全是边界:
const http = require('http');
const server = http.createServer((req, res) => {
res.end('ok');
});
server.headersTimeout = 60_000; // 请求头必须在 60s 内收完,防慢速攻击
server.requestTimeout = 300_000; // 整个请求最多收 300s
server.keepAliveTimeout = 65_000; // 空闲长连接留 65s,要比前面负载均衡的空闲超时长
server.on('error', (err) => { // 监听失败,比如端口被占
console.error('listen failed:', err.code);
process.exit(1);
});
server.listen(process.env.PORT || 3000, () => {
console.log('listening on', server.address().port);
});
process.on('SIGTERM', () => { // 优雅停机:不接新连接,等存量请求做完
server.close(() => process.exit(0));
});
读这个模块可以按一条主线:连接进来(connection)、请求头解析完(request)、读 body、写响应、连接复用或关闭、服务停机(close)。每个阶段都有对应的超时和异常事件,面试被问到某个事件或配置时,先把它放回这条线上的位置,就知道它防的是什么。
面试官追问
追问 1Node 服务挂在负载均衡后面,偶发 502,服务日志里什么都没有,先看什么?
先看 server.keepAliveTimeout,默认只有 5s。负载均衡的空闲超时如果是 60s,它会拿一条 Node 已经关掉的连接去发请求。把 keepAliveTimeout 调到比负载均衡长,比如 65s,老版本还要让 headersTimeout 再大一点。
追问 2有人慢悠悠地一个字节一个字节发请求头拖住连接,Node 靠哪个配置防?
headersTimeout,规定请求头必须多久内发完,默认 60s;requestTimeout 再兜住整个请求。超时后 Node 直接回 408 并关连接。
追问 3cluster 起了 4 个进程监听 3000,用户登录状态存在内存里,为什么时有时无?
每个进程各有一份内存,请求落到哪个进程不确定,登录写在进程一,下次请求落到进程二就没了。会话放 Redis 或者改用 JWT,别存进程内存。
追问 4http.createServer 和 Express 是什么关系?
Express 的 app 本身就是一个 (req, res) => {} 函数,app.listen() 内部还是 http.createServer(app)。它没换掉 Node 的 HTTP 层,只是在 request 回调里加了路由和中间件。
# http服务端概览
30 秒速记
- 一次请求在服务端走的路:
TCP建连(connection)→ 请求头解析完(request)→ 读body(data/end)→ 写响应(res.end)→ 连接复用或关闭 - 三个角色:
http.Server管全局和事件,req(IncomingMessage)只读,res(ServerResponse)只写 request回调触发时只有请求头,body还在路上,要自己读- 所有请求的回调都在一个主线程上跑,回调里的同步重活会卡住所有人
- 出了问题按阶段找:连不上看监听,
400看clientError,一直转圈看哪个分支漏了res.end()
服务端处理一个请求就是一条流水线:建连、解析请求头、读请求体、写响应,最后决定连接是复用还是关掉。 Node 把它拆成几个对象:http.Server 负责接连接、发事件,req 是只读的请求流,res 是只写的响应流。新人最常误解的是 request 回调触发时请求体还没到,它只代表请求头解析完了。排查问题我也按这几个阶段去定位:连不上看监听和端口,报 400 看 clientError,一直转圈多半是哪个分支漏了 res.end()。
理解「http服务端概览」时,要先明确输入、运行阶段和可观察输出。这里的交互实验会分别执行正常路径与失败路径,并把 Node.js 的关键状态变化和同屏代码对应起来;不能只记命令名称。
工程中还要验证退出码、资源清理和异常输入。属于 network 场景的配置应保持最小可复现,并在开发环境与 CI 中使用相同命令复跑,避免把平台差异或隐式默认值带到线上。
面试官追问
追问 1接口偶发一直转圈不返回,代码里也没报错,最可能是什么?
某个分支没调 res.end(),比如提前 return 了,或者异步回调里的异常被吞了。Node 不会替你结束响应,最后只能等客户端或网关超时。每个分支都要有出口,或者交给框架的统一错误处理。
追问 2res.end() 之后再调 res.setHeader() 会怎样?
抛 ERR_HTTP_HEADERS_SENT,响应头已经跟着第一块数据发出去了。设置头要在 res.write() / res.end() 之前,Express 里那个 Cannot set headers after they are sent 报错就是这么来的。
追问 3为什么 request 回调里直接拿不到 POST 的数据?
回调触发时 Node 只解析完了请求头,body 还在一块块地来。要监听 data 攒起来、在 end 里用,或者 for await (const chunk of req)。
追问 4一个请求进来,Node 是开一个线程去处理吗?
不是,所有请求的回调都在同一个主线程上跑,靠事件循环轮流处理。所以回调里一个同步的大循环会把所有请求一起卡住,CPU 重活要丢给 worker_threads 或者拆成单独的服务。
# 创建server
30 秒速记
- 一行就能起:
http.createServer((req, res) => res.end('ok')).listen(3000) http.createServer(fn)和new http.Server(fn)等价,fn被挂成request事件的监听器listen是异步的:成功走回调 /listening事件,失败走error事件,不会在那一行抛出来- 每个请求都要有出口,
res.end()不调,客户端就一直挂着 - 端口别写死:
process.env.PORT || 3000;容器里要能被外部访问,别绑127.0.0.1
创建服务就是 http.createServer(requestListener) 再 listen(端口),requestListener 会在每个请求到来时被调用,拿到 req 和 res。 createServer 和 new http.Server 是一回事,传进去的函数本质上就是 request 事件的监听器,写成 server.on('request', fn) 效果一样。要注意 listen 是异步的,端口被占不会在这一行抛出来,而是走 error 事件,所以我一般顺手挂上 error 监听,在 listen 的回调里再打「启动成功」。
几行代码搞定
var